"""   Copyright 2011 Bart Enkelaar

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
"""

from tweetcleaner import TextExtractor
import auth_dat
import logging
import re
import sys

sys.path.insert(0, 'tweepy.zip')
from tweepy import TweepError, API, OAuthHandler



class Match():
    """Stores two Line objects that should be after each other in a song."""
    def __init__(self, lineOne, lineTwo):
        self.lineOne = lineOne
        self.lineTwo = lineTwo
        self.d_ratio = abs(1.0-(float(len(lineOne.sentence))/float(len(lineTwo.sentence))))
    
    def get_weight(self):
        return self.lineOne.weight + self.lineTwo.weight
    
    def __lt__(self, o):
        if self.get_weight() != o.get_weight():
            return self.get_weight() < o.get_weight()
        return self.d_ratio > o.d_ratio

    def __gt__(self, o):
        return not self < o
    
    def __eq__(self, o):
        return (isinstance(o, Match) and 
                self.lineOne == o.lineOne and 
                self.lineTwo == o.lineTwo)
    
    def __ne__(self, o):
        return not self == o



class Line():
    """Stores information about a sentence to be used in a song."""
    def __init__(self, sentence, weight=1):
        self.sentence = sentence
        self.weight = weight
        
    def __eq__(self, o):
        return isinstance(o, Line) and self.sentence == o.sentence and self.weight == o.weight
    
    def __ne__(self, o):
        return not self == o
    
    def __hash__(self):
        code = 7
        code += 32 * code + hash(self.sentence)
        code += 32 * code + self.weight
        return code
    
    def __lt__(self, o):
        if self.weight > o.weight:
            return False
        return self.weight < o.weight or len(self.sentence) < len(o.sentence)
    
    def __gt__(self, o):
        return o < self
        
    def __str__(self):
        return "[%s, weight: %s]" % (self.sentence, self.weight)



class SongBuilder():
    """Class that can be used to build a song, does the querying of twitter."""
    class Song():
        """Wrapper object for a song"""
        def __init__(self, verses, lines_per_verse, lines_per_chorus):
            logging.debug("Creating a song with %s verses, %s lines per verse and %s lines per chorus." %
                          (verses, lines_per_verse, lines_per_chorus))
            self.verses = verses
            self.lines_per_verse = lines_per_verse
            self.lines_per_chorus = lines_per_chorus
            self.verse_lines = []
            self.chorus_lines = []
        
        def __str__(self):
            print "[chorus: %s; verses: %s]" % (self.chorus_lines, self.verse_lines)
    
    def __init__(self, subject, verses=2, lines_per_verse=3, lines_per_chorus=2, page_step=5, line_limit=75):
        self.subject = subject
        self.verses = verses
        self.lines_per_verse = lines_per_verse
        self.lines_per_chorus = lines_per_chorus
        self.page_step = page_step
        self.extractor = TextExtractor(self.subject, line_limit)
    
    def weigh(self, list, resultDict=None):
        """Creates Line objects for the sentences in the list, returns a
        dictionary mapping sentences to line objects. (Removing doubles
        and turning them into lines with a heavier weight)
        """
        if resultDict is None:
            resultDict = dict()
        for line in list:
            if line in resultDict:
                resultDict[line].weight += 1
            else:
                resultDict[line] = Line(line)
        return resultDict

    def create(self):
        """Creates a song with the variables passed to the constructor"""
        lines = dict()
        page = 1
        song = SongBuilder.Song(self.verses, self.lines_per_verse, self.lines_per_chorus)
        
        try:
            while ((not song or len(song.verse_lines) < (self.lines_per_verse * self.verses)) and page <= 30):
                song, lines, page = self._create_from_pages(lines, page)
        except TweepError, e:
            logging.debug("TweepError raised: %s" % e)
            song.chorus_lines.append(Match(Line("ERROR"), Line("Twitter API error: %s" % e)))
            
        return song

    def _create_from_pages(self, lines, page):
        song = SongBuilder.Song(self.verses, self.lines_per_verse, self.lines_per_chorus)
        before_lines = len(lines)
        for page in range(page, page + self.page_step):
            logging.debug("Checking page %s with subject '%s'" % (page, self.subject))
            lines, sorted_lines = self._load_lines(lines, page)
        page += 1
        
        if len(lines) > before_lines:
            logging.debug("Searching through %s lines" % len(lines))
            song = self._build_song(sorted_lines)
        return song, lines, page

    def _build_song(self, sorted_lines):
        matches = self._find_all_matches(sorted_lines)
        return self.build_verses(matches)
    
    def _load_lines(self, lines, page):
        sorted_lines, line_dict = self._get_sorted_list(lines, page)
        lines = self._join_dicts(lines, line_dict)
        return lines, sorted_lines

    def _join_dicts(self, lines, line_dict):
        for k, v in line_dict.iteritems():
            if k in lines:
                lines[k].weight += 1
            else:
                lines[k] = v
        return lines

    def build_verses(self, matches):
        """Takes a list of Match objects and turns them into a Song"""
        song = SongBuilder.Song(self.verses, self.lines_per_verse, self.lines_per_chorus)
        matches = sorted(matches, reverse=True)
        for i in range(0, len(matches)):
            if i < self.lines_per_chorus:
                song.chorus_lines.append(matches[i])
            elif i < (self.verses * self.lines_per_verse + self.lines_per_chorus):
                song.verse_lines.append(matches[i])
            else:
                break
        return song
                
    def _get_sorted_list(self, lines, page):
        source_lines = self._create_line_list(page)
        if len(lines) == 0:
            lines = self.weigh(source_lines)
        else:
            lines = self.weigh(source_lines, lines)
        return sorted(lines.values(), reverse=True), lines

    def _create_line_list(self, page):
        auth = OAuthHandler(auth_dat.CONSUMER_KEY, auth_dat.CONSUMER_SECRET)
        tweets = [t.text for t in self._get_sources(API(
                auth_handler=auth), page)]
        sentences = []
        for tweet in tweets:
            sentences += self.extractor.extract_english(tweet) 
        return self.weigh(sentences)

    def _get_sources(self, tweets, page, per_page=100):
        return tweets.search(self.subject, page=page, rpp=per_page, lang="en")

    def _find_all_matches(self, sorted_lines):
        matches = []
        matched = []
        for index in range(0, len(sorted_lines)):
            base_line = sorted_lines[index]
            if base_line in matched:
                continue
            matched_here = self._filter_matches(base_line,
                            self._filter_unmatched(matched, index, sorted_lines))
            if matched_here:
                best_match = sorted(matched_here, reverse=True)[0]
                matches.append(best_match)
                matched.append(best_match.lineTwo)
        return matches

    def _filter_unmatched(self, matched, i, sorted_lines):
        return filter(lambda x:x not in matched, sorted_lines[i + 1:])

    def _filter_matches(self, base_line, other_lines):
        return [Match(base_line, line) for line in
                filter(RhymeMatcher(base_line).matches, other_lines)]



RHYME_REGEX = r"[aeiou]+(?:[^aeiou]+)?(?:[aiou]|e[ynsrd]?)?\b"

class RhymeMatcher():
    """Class that can be used to match rhyming in a sentence (poorly)"""
    def __init__(self, line):
        self.text = line.sentence
        self.rhyme = self._determine_rhyme(self._find_last_word(self.text.lower()))
    
    def matches(self, line):
        """Returns a boolean whether the line of this matcher matches
        the line passed into the method.
        """
        if not self.rhyme:
            return False
        otherRhyme = self._determine_rhyme(self._find_last_word(line.sentence.lower()))
        if not otherRhyme:
            return False
        if self.rhyme == otherRhyme:
            selfend = self._find_last_word(self.text.lower())
            lineend = self._find_last_word(line.sentence.lower())
            return selfend != lineend and not selfend.endswith(lineend) and not lineend.endswith(selfend)
        return False

    def _find_last_word(self, sentence):
        return sentence.split(" ")[-1].strip(" !)(?.,")

    def _determine_rhyme(self, word):
        assert " " not in word.strip()
        m = re.search(RHYME_REGEX, word)
        return m.group(0) if m else None
    


def printLines(lines):
    for line in lines:
        print line.lineOne.sentence
        print line.lineTwo.sentence

if __name__ == '__main__':
    logging.getLogger().setLevel(logging.DEBUG)
    song = SongBuilder("geektrip").create()
    if not song:
        sys.exit(0)
    printLines(song.chorus_lines)
    print ""
    for i in range(0, song.verses):
        printLines(song.verse_lines[song.lines_per_verse * i:song.lines_per_verse * (i + 1)])
        print ""
        printLines(song.chorus_lines)
        print ""
    printLines(song.chorus_lines)